package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.product.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TestServiceImpl implements TestService {

   @Autowired
   private StringRedisTemplate redisTemplate;


    //加锁 单体项目有效 多进程失效
   @Override
//   public synchronized void testLock() {
//
//      // 查询Redis中的num值
//      String value = this.stringRedisTemplate.opsForValue().get("num");
//      // 没有该值return
//      if (StringUtils.isBlank(value)){
//         return ;
//      }
//      // 有值就转成int
//      int num = Integer.parseInt(value);
//      // 把Redis中的num值+1
//      this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
//
//   }

   //分布式锁
   public synchronized void testLock() {
      //0.TODO尝试从缓存中获取业务数据,如果命中缓存则返回即可,缓存未命中,解决击穿问题,先获取分布式锁
      //1.获取分布式锁
      //1.1 版本1 :采用redis提供的setnx命令 存在问题: A线程加锁成功后,逻辑执行中服务宕机,锁无法释放造成死锁
      String lockName="lock";
//      String lockValue="lock";
//      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue);
       //1.2 版本2 :在版本1的基础上,为锁的key设置过期时间,防止死锁  问题 :加锁跟设置锁过期时间不具备原子性
       //       Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue);
//       //防止死锁,通过expire命令为锁的Key设置过期时间
//       redisTemplate.expire(lockName,5, TimeUnit.SECONDS);
//       //成功获得锁才执行业务逻辑
       //1.3 版本3 : 使用set命令配合NX EX参数 替换版本2的命令 确保写入值和设置过期时间的原子性
       // 问题: 1阻塞 过时释放锁 2执行结果1通了 1线程执行锁的释放释放了2的锁
//       Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);
       //1.4 版本4: 在版本3的基础上,存入Redis锁的值,为每个线程生成UUID作为线程标识
       // 问题 : t1锁判断成功准备释放结果过期了(redis释放的) t2获取到锁 之后t1继续执行锁的释放 t1释放了t2的锁
       String lockValue = UUID.randomUUID().toString();
       Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);
       //2.执行业务逻辑
      if(flag){
         try {
            // 查询Redis中的num值
            String value = (String) this.redisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)){
               return ;
            }
            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
         } finally {
            //3.TODO 释放锁
//            redisTemplate.delete(lockName);
             //4.版本4: 先判断Redis中锁的值跟当前线程虚拟机栈获取当前线程标识值是否一致,一致则删除锁
//             if(redisTemplate.opsForValue().get(lockName).equals(lockValue)){
//                 redisTemplate.delete(lockName);
//             }
             //5.版本5：使用lua脚本删除锁，确保删除锁跟判断锁值一致性
             // 问题：1.编码麻烦  2.底层采用String无法锁可重入  3.加锁后没有锁自动续期机制  版本6：采用生产框架Redisson提供分布式锁
             //5.1 创建Redis脚本对象，封装lua脚本
             String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                     "then\n" +
                     "    return redis.call(\"del\",KEYS[1])\n" +
                     "else\n" +
                     "    return 0\n" +
                     "end\n";
             RedisScript<Boolean> booleanRedisScript = new DefaultRedisScript<>(script, Boolean.class);
             redisTemplate.execute(booleanRedisScript, Arrays.asList(lockName), lockValue);
         }
      }else{
         log.error("获取锁失败:{},再次尝试自旋",Thread.currentThread().getName());
          try {
              Thread.sleep(50);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          this.testLock();
      }

   }
}